package com.test.demo.util;

import org.locationtech.jts.algorithm.locate.SimplePointInAreaLocator;
import org.locationtech.jts.awt.GeometryCollectionShape;
import org.locationtech.jts.awt.PointTransformation;
import org.locationtech.jts.awt.ShapeReader;
import org.locationtech.jts.awt.ShapeWriter;
import org.locationtech.jts.geom.*;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.geom.impl.PackedCoordinateSequenceFactory;
import org.locationtech.jts.operation.polygonize.Polygonizer;
import org.locationtech.jts.util.GeometricShapeFactory;

import java.awt.*;
import java.awt.geom.*;
import java.util.*;
import java.util.List;
import java.util.stream.Collectors;

public class GeometryTools {
    private final static GeometryFactory DEFAULT_FACTORY = new GeometryFactory(
            new PrecisionModel(100.0),
            0,
            PackedCoordinateSequenceFactory.FLOAT_FACTORY);
    private static GeometryConverter DEFAULT_INSTANCE = new GeometryConverter.Builder()
            .build();


    public static Geometry shapeToGeometry(Shape shape) {
        var geometry = DEFAULT_INSTANCE.shapeToGeometry(shape);
        return geometry;
    }

    public static class GeometryConverter {
        public static class Builder {

            private GeometryFactory factory;

            private double pixelHeight = 1;
            private double pixelWidth = 1;
            private double flatness = 0.5;

            /**
             * Default constructor for a builder with flatness 0.5 and pixel width/height of 1.0.
             */
            public Builder() {}

            /**
             * Specify the pixel width and height, used to scale x and y coordinates during conversion (default is 1.0 for both).
             * @param pixelWidth
             * @param pixelHeight
             * @return
             */
            public Builder pixelSize(double pixelWidth, double pixelHeight) {
                this.pixelWidth = pixelWidth;
                this.pixelHeight = pixelHeight;
                return this;
            }

            /**
             * Specify the flatness for any operation where a PathIterator is required.
             * @param flatness
             * @return
             */
            public Builder flatness(double flatness) {
                this.flatness = flatness;
                return this;
            }

            /**
             * Specify the GeometryFactory, which can define a precision model in JTS.
             * @param factory
             * @return
             */
            public Builder factory(GeometryFactory factory) {
                this.factory = factory;
                return this;
            }

            /**
             * Build a new converter with the specified parameters.
             * @return
             */
            public GeometryConverter build() {
                return new GeometryConverter(factory, pixelWidth, pixelHeight, flatness);
            }

        }
        private GeometryFactory factory;

        private double pixelHeight, pixelWidth;
        private double flatness = 0.1;

        private AffineTransform transform = null;
        private Transformer transformer;

        private ShapeReader shapeReader;


        private GeometryConverter(final GeometryFactory factory, final double pixelWidth, final double pixelHeight, final double flatness) {
            if (factory == null) {
                if (pixelWidth == 1 && pixelHeight == 1)
                    this.factory = DEFAULT_FACTORY;
                else
                    this.factory = new GeometryFactory(new PrecisionModel(PrecisionModel.FLOATING_SINGLE), 0, PackedCoordinateSequenceFactory.FLOAT_FACTORY);
            } else
                this.factory = factory;
            this.flatness = flatness;
            this.pixelHeight = pixelHeight;
            this.pixelWidth = pixelWidth;
            if (pixelWidth != 1 && pixelHeight != 1)
                transform = AffineTransform.getScaleInstance(pixelWidth, pixelHeight);
            this.transformer = new Transformer();
        }


        private Geometry shapeToGeometry(Shape shape) {
            if (shape instanceof Area)
                return areaToGeometry((Area) shape);
            PathIterator iterator = shape.getPathIterator(transform, flatness);
            if ((shape instanceof Path2D || shape instanceof GeneralPath) && containsClosedSegments(iterator)) {
                // Arbitrary paths that correspond to an area can fail with JTS ShapeReader, so convert to areas instead
                return shapeToGeometry(new Area(shape));
            } else
                iterator = shape.getPathIterator(transform, flatness);
            return getShapeReader().read(iterator);
        }

        /**
         * Test of an iterator contains closed segments, indicating the iterator relates to an area.
         *
         * @param iterator
         * @return true if any SEG_CLOSE segments are found
         */
        private static boolean containsClosedSegments(PathIterator iterator) {
            double[] coords = new double[6];
            while (!iterator.isDone()) {
                iterator.next();
                if (iterator.currentSegment(coords) == PathIterator.SEG_CLOSE)
                    return true;
            }
            return false;
        }

        private Geometry areaToGeometry(Area shape) {
            return convertAreaToGeometry(shape, transform, flatness, factory);
        }


        /**
         * Convert a java.awt.geom.Area to a JTS Geometry, trying to correctly distinguish holes.
         * <p>
         *
         * @param area
         * @param transform
         * @param flatness
         * @param factory
         * @return a geometry corresponding to the Area object
         * @implNote An alternative (more complex) method was used in QuPath v0.2.0, using JTS 1.16.1.
         * The one advantage of the older method was that it used {@link CoordinateXY} - however this
         * resulted in test failures in JTS v1.17.0, caused by mixed-dimension coordinates
         * being generated within some operations as described by https://github.com/locationtech/jts/issues/434
         * Consequently, there may be some loss of efficiency.
         * <p>
         * See also https://github.com/locationtech/jts/issues/408
         */
        private static Geometry convertAreaToGeometry(final Area area, final AffineTransform transform, final double flatness, final GeometryFactory factory) {

            PathIterator iter = area.getPathIterator(transform, flatness);

            PrecisionModel precisionModel = factory.getPrecisionModel();
            Polygonizer polygonizer = new Polygonizer(true);

            java.util.List<Coordinate[]> coords = (java.util.List<Coordinate[]>) ShapeReader.toCoordinates(iter);
            java.util.List<Geometry> geometries = new ArrayList<>();
            for (Coordinate[] array : coords) {
                for (var c : array)
                    precisionModel.makePrecise(c);

                LineString lineString = factory.createLineString(array);
                geometries.add(lineString);
            }
            polygonizer.add(factory.buildGeometry(geometries).union());
            return polygonizer.getGeometry();

        }

        private ShapeReader getShapeReader() {
            if (shapeReader == null)
                shapeReader = new ShapeReader(factory);
            return shapeReader;
        }


        private ShapeWriter getShapeWriter() {
            return new ShapeWriter(transformer);
        }


        private class Transformer implements PointTransformation {

            @Override
            public void transform(Coordinate src, Point2D dest) {
                dest.setLocation(
                        src.x / pixelWidth,
                        src.y / pixelHeight);
            }

        }
    }
}
